﻿#region Header
// ------------------------ Licence / Copyright ------------------------
// 
// ScrumTable for Scrum Meeting Support
// Copyright © HSR - Hochschule für Technik Rapperswil
// All Rights Reserved
// 
// Author:
//  Michael Gfeller Silvan Gehrig Patrick Boos
// 
// ---------------------------------------------------------------------
#endregion

#region Usings

using System;
using System.Collections.Generic;
using System.ComponentModel;
using ScrumTable.BL.DM.DataManagement.Logging;
using ScrumTable.BL.DM.DataManagement.Tracking.EditCommands;
using ScrumTable.BL.DM.Properties;
using ScrumTable.Common;
using ScrumTable.Common.Collections;
using ScrumTable.Common.EventHandling;
using ScrumTable.Common.Logging;

#endregion

namespace ScrumTable.BL.DM.DataManagement.Tracking
{
    /// <summary>
    /// Represents the undo/redo manager for all business objects in the
    /// current data store. This class can be used to inform the user about
    /// the undo/redo actions.
    /// </summary>
    public class UndoManager : UndoMultipleTrackingBase, INotifyPropertyChanged
    {
        #region Declarations
        //--------------------------------------------------------------------
        // Declarations
        //--------------------------------------------------------------------

        private const int InitialLimit = 50;
        private const string EvtKeyPropertyChanged = "PropertyChanged";

        private readonly DelegateHandlerCollection<PropertyChangedEventHandler> _events = new DelegateHandlerCollection<PropertyChangedEventHandler>();
        private readonly IDomainContext _context;
        private readonly Stack<IUndoEditCommand> _undoEntries = new Stack<IUndoEditCommand>(InitialLimit);
        private readonly Stack<IUndoEditCommand> _redoEntries = new Stack<IUndoEditCommand>(InitialLimit);

        private int _limit = InitialLimit;

        #endregion

        #region Properties
        //--------------------------------------------------------------------
        // Properties
        //--------------------------------------------------------------------

        /// <summary>
        /// Gets/sets the maximum undo/redo item number.
        /// </summary>
        public int Limit
        { 
            get { return _limit; }
            set
            {
                PreCondition.AssertInRange(0, 1024, value, "value");
                _limit = value;
                OnPropertyChanged(new PropertyChangedEventArgs("Limit"));
            }
        }

        /// <summary>
        /// Returns true if the current undo manager can perform an undo action.
        /// </summary>
        public bool CanUndo { get { return HasEntries(_undoEntries.Count); } }

        /// <summary>
        /// Returns true if the current undo manager can perform a redo action.
        /// </summary>
        public bool CanRedo { get { return HasEntries(_redoEntries.Count); } }

        /// <summary>
        /// Gets a read only copy of the undo stack.
        /// </summary>
        public ICollection<IUndoEdit> UndoStack
        {
            get { return EnumerableUtil.Convert<IUndoEdit>(_undoEntries); }
        }

        /// <summary>
        /// Gets a read only copy of the redo stack.
        /// </summary>
        public ICollection<IUndoEdit> RedoStack
        {
            get { return EnumerableUtil.Convert<IUndoEdit>(_redoEntries); }
        }

        #endregion

        #region Constructors / Destructor
        //--------------------------------------------------------------------
        // Constructors / Destructor
        //--------------------------------------------------------------------

        /// <summary>
        /// Creates a new UndoManager instance.
        /// </summary>
        internal UndoManager(IDomainContext context)
        {
            PreCondition.AssertNotNull(context, "context");

            _context = context;
        }

        #endregion

        #region Methods
        //--------------------------------------------------------------------
        // Methods
        //--------------------------------------------------------------------

        /// <summary>
        /// Clears all undo/redo entries of the current instance.
        /// </summary>
        public void DiscardAllEdits()
        {
            WatchForUndoRedoChange(
                () =>
                    {
                        _undoEntries.Clear();
                        _redoEntries.Clear();
                    });
        }

        /// <summary>
        /// Calls the redo action on the top of the redo stack and pushs onto
        /// the undo stack.
        /// </summary>
        public void Redo()
        {
            if (!CanRedo)
                return;

            ActionLogger.InfoFormat(Resources.LogActUndoMngrRedo);

            WatchForUndoRedoChange(
                () =>
                    {
                        IUndoEditCommand editCommand = _redoEntries.Pop();
                        editCommand.CallRedo();
                        _undoEntries.Push(editCommand);
                    });
        }

        /// <summary>
        /// Calls the undo action on the top of the undo stack and pushs onto
        /// the redo stack.
        /// </summary>
        public void Undo()
        {
            if (!CanUndo)
                return;

            ActionLogger.InfoFormat(Resources.LogActUndoMngrUndo);

            WatchForUndoRedoChange(
                () =>
                    {
                        IUndoEditCommand editCommand = _undoEntries.Pop();
                        editCommand.CallUndo();
                        _redoEntries.Push(editCommand);
                    });
        }

        /// <summary>
        /// Adds a new edit entry to the undo stack. Calling this method will
        /// clear the redo stack.
        /// </summary>
        /// <param name="editCommand">Specifies the entry to push onto the undo stack.</param>
        internal override void AddEdit(IUndoEditCommand editCommand)
        {
            PreCondition.AssertNotNull(editCommand, "editCommand");

            // initialize the command for the current manager
            editCommand.Context = _context;
            base.AddEdit(editCommand);
        }

        /// <summary>
        /// Adds a new edit entry to the current undo stack. This call will ignore
        /// the multiple undo/redo action request.
        /// </summary>
        /// <param name="editCommand">Specifies the entry to push onto the undo stack.</param>
        internal override void AddEditToCurrent(IUndoEditCommand editCommand)
        {
            WatchForUndoRedoChange(
            () =>
            {
                if (_undoEntries.Count == Limit)
                    ResizeStack(_undoEntries, 1);

                _redoEntries.Clear();
                _undoEntries.Push(editCommand);
            }); 
        }

        /// <summary>
        /// Creates a new instance of an UndoMultipleTrackingBase object.
        /// </summary>
        /// <param name="actionName">Name of the underlying undo/redo element.</param>
        /// <param name="doBeforeUndoRedo">Specifies the action to be executed before performing undo/redo.</param>
        /// <param name="doAfterUndoRedo">Specifies the action to be executed before performing undo/redo.</param>
        /// <returns>Returns tue created object.</returns>
        protected override UndoMultipleTrackingCommandBase CreateMultipleTrackingCommand(string actionName,
                                                                                  Action doBeforeUndoRedo,
                                                                                  Action doAfterUndoRedo)
        {
            return new MultipleTrackingUndoCommand(actionName, doBeforeUndoRedo, doAfterUndoRedo);
        }

        private void ResizeStack(Stack<IUndoEditCommand> toResize, int numCutOff)
        {
            IUndoEditCommand[] localEntries = new IUndoEditCommand[Limit];
            toResize.CopyTo(localEntries, 0);
            toResize.Clear();

            for (int i = numCutOff; i < localEntries.Length; ++i)
            {
                if (localEntries[i] != null)
                    toResize.Push(localEntries[i]);
            }
        }

        private void WatchForUndoRedoChange(Action toExecute)
        {
            int beforeUndo = _undoEntries.Count;
            int beforeRedo = _redoEntries.Count;
            bool isFull = (beforeUndo == Limit);

            try
            {
                toExecute();
            }
            catch (Exception e)
            {
                Logger<UndoManager>.Error(Resources.ExcUndoRedoMngrExecute, e);
                _undoEntries.Clear();
                _redoEntries.Clear();
            }

            if (beforeUndo != _undoEntries.Count || isFull) OnPropertyChanged(new PropertyChangedEventArgs("UndoStack"));
            if (beforeRedo != _redoEntries.Count || isFull) OnPropertyChanged(new PropertyChangedEventArgs("RedoStack"));
            if (HasEntries(beforeUndo) != CanUndo) OnPropertyChanged(new PropertyChangedEventArgs("CanUndo"));
            if (HasEntries(beforeRedo) != CanRedo) OnPropertyChanged(new PropertyChangedEventArgs("CanRedo"));
        }

        private bool HasEntries(int count) { return count > 0; }

        #endregion

        #region Events
        //--------------------------------------------------------------------
        // Events
        //--------------------------------------------------------------------

        /// <summary>
        /// Sends the property changed.
        /// </summary>
        /// <param name="eventArgs">Contains the name of the property.</param>
        protected void OnPropertyChanged(PropertyChangedEventArgs eventArgs)
        {
            _events.Fire(EvtKeyPropertyChanged, this, eventArgs);
        }

        /// <summary>
        /// Occurs when a property value changes.
        /// </summary>
        public event PropertyChangedEventHandler PropertyChanged
        {
            add { _events.Add(EvtKeyPropertyChanged, value); }
            remove { _events.Remove(EvtKeyPropertyChanged, value); }
        }

        #endregion


    }
}